Laravel Session——session 的启动与运行源码分析

前言

在网页开发中, session 具有重要的作用,它可以在多个请求中存储用户的信息,用于识别用户的身份信息。laravel 为用户提供了可读性强的 API 处理各种自带的 Session 后台驱动程序。支持诸如比较热门的 Memcached、Redis 和开箱即用的数据库等常见的后台驱动程序。本文将会在本篇文章中讲述最常见的由 Fileredis 驱动的 session 源码。

session 服务的注册

与其他功能一样,session 由自己的服务提供者在 container 内进行注册:

  1. class SessionServiceProvider extends ServiceProvider
  2. {
  3. public function register()
  4. {
  5. $this->registerSessionManager();
  6. $this->registerSessionDriver();
  7. $this->app->singleton(StartSession::class);
  8. }
  9. protected function registerSessionManager()
  10. {
  11. $this->app->singleton('session', function ($app) {
  12. return new SessionManager($app);
  13. });
  14. }
  15. protected function registerSessionDriver()
  16. {
  17. $this->app->singleton('session.store', function ($app) {
  18. return $app->make('session')->driver();
  19. });
  20. }
  21. }

可以看到 SessionManager 是整个 session 服务的接口类,一切对 session 的操作都是由这个类实现。session.storesession 服务的存储驱动。

session 服务的启动

session 服务是以中间件的形式启动的,其中间件是 Illuminate\Session\Middleware\StartSession:

  1. public function handle($request, Closure $next)
  2. {
  3. $this->sessionHandled = true;
  4. if ($this->sessionConfigured()) {
  5. $request->setLaravelSession(
  6. $session = $this->startSession($request)
  7. );
  8. $this->collectGarbage($session);
  9. }
  10. $response = $next($request);
  11. if ($this->sessionConfigured()) {
  12. $this->storeCurrentUrl($request, $session);
  13. $this->addCookieToResponse($response, $session);
  14. }
  15. return $response;
  16. }
  17. public function terminate($request, $response)
  18. {
  19. if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
  20. $this->manager->driver()->save();
  21. }
  22. }

session 服务的中间件在 http 会话前与会话后都有处理。

在会话前,

  • laravel 试图从 cookies 中获取 sessionId
  • 利用 sessionId 读取服务器中的 session 数据;
  • session 对象存入 request 中;
  • session 垃圾回收

在会话后,

  • 存储当前的 url 作为 sessionPreviousUrl
  • 将当前的 session 存入浏览器 cookies
  • 保存当前的 session 数据到存储器驱动

startSession

startSession 函数进行了 session 的启动工作:

  1. public function __construct(SessionManager $manager)
  2. {
  3. $this->manager = $manager;
  4. }
  5. protected function startSession(Request $request)
  6. {
  7. return tap($this->getSession($request), function ($session) use ($request) {
  8. $session->setRequestOnHandler($request);
  9. $session->start();
  10. });
  11. }
  12. public function getSession(Request $request)
  13. {
  14. return tap($this->manager->driver(), function ($session) use ($request) {
  15. $session->setId($request->cookies->get($session->getName()));
  16. });
  17. }

session 的门面类 sessionManager

代码很简洁,session 服务启动的逻辑被包含在了 sessionManager 中,sessionManagersession 服务的门面类,负责 session 服务的驱动加载与数据操作。

首先我们先看看 SessionManager:

  1. namespace Illuminate\Session;
  2. use Illuminate\Support\Manager;
  3. class SessionManager extends Manager
  4. {
  5. }

SessionManager 继承 Manager 类:

  1. namespace Illuminate\Support;
  2. abstract class Manager
  3. {
  4. public function driver($driver = null)
  5. {
  6. $driver = $driver ?: $this->getDefaultDriver();
  7. if (! isset($this->drivers[$driver])) {
  8. $this->drivers[$driver] = $this->createDriver($driver);
  9. }
  10. return $this->drivers[$driver];
  11. }
  12. }

当我们调用 driver 函数的时候,程序就开始为 session 服务加载驱动,例如对数据库或者 redis 驱动,进行 连接 操作。

  1. public function getDefaultDriver()
  2. {
  3. return $this->app['config']['session.driver'];
  4. }
  5. protected function createDriver($driver)
  6. {
  7. $method = 'create'.Str::studly($driver).'Driver';
  8. if (isset($this->customCreators[$driver])) {
  9. return $this->callCustomCreator($driver);
  10. } elseif (method_exists($this, $method)) {
  11. return $this->$method();
  12. }
  13. throw new InvalidArgumentException("Driver [$driver] not supported.");
  14. }

session 驱动持久化类 SessionHandler

FileSessionHandler 这个类就是驱动,它继承 SessionHandlerInterface 基类,任何对 session 的读取、添加、删除、更新等等操作最后都要通过这个驱动类进行持久化。

  • file 驱动:

file 驱动的核心是 Filesystem,该类是 Ioc 容器创建的:

  1. protected function createFileDriver()
  2. {
  3. return $this->createNativeDriver();
  4. }
  5. protected function createNativeDriver()
  6. {
  7. $lifetime = $this->app['config']['session.lifetime'];
  8. return $this->buildSession(new FileSessionHandler(
  9. $this->app['files'], $this->app['config']['session.files'], $lifetime
  10. ));
  11. }
  12. namespace Illuminate\Session;
  13. class FileSessionHandler implements SessionHandlerInterface
  14. {
  15. public function __construct(Filesystem $files, $path, $minutes)
  16. {
  17. $this->path = $path;
  18. $this->files = $files;
  19. $this->minutes = $minutes;
  20. }
  21. }
  • redis 驱动

redis 驱动并不是直接创建 redis,而是利用了 laravel 的缓存 cache 系统创建 redis 驱动,然后对 redis 驱动进行连接操作:

  1. protected function createRedisDriver()
  2. {
  3. $handler = $this->createCacheHandler('redis');
  4. $handler->getCache()->getStore()->setConnection(
  5. $this->app['config']['session.connection']
  6. );
  7. return $this->buildSession($handler);
  8. }
  9. protected function createCacheHandler($driver)
  10. {
  11. $store = $this->app['config']->get('session.store') ?: $driver;
  12. return new CacheBasedSessionHandler(
  13. clone $this->app['cache']->store($store),
  14. $this->app['config']['session.lifetime']
  15. );
  16. }
  17. class CacheBasedSessionHandler implements SessionHandlerInterface
  18. {
  19. public function __construct(CacheContract $cache, $minutes)
  20. {
  21. $this->cache = $cache;
  22. $this->minutes = $minutes;
  23. }
  24. }

session 数据操作类

buildSession 函数将会返回 Store 类,这个 Store 类实际上 session 服务数据操作的实质类,任何对 session 数据的操作实际上调用的都是 Store 类:

  1. protected function buildSession($handler)
  2. {
  3. if ($this->app['config']['session.encrypt']) {
  4. return $this->buildEncryptedSession($handler);
  5. } else {
  6. return new Store($this->app['config']['session.cookie'], $handler);
  7. }
  8. }
  9. protected function buildEncryptedSession($handler)
  10. {
  11. return new EncryptedStore(
  12. $this->app['config']['session.cookie'], $handler, $this->app['encrypter']
  13. );
  14. }
  15. public function __call($method, $parameters)
  16. {
  17. return $this->driver()->$method(...$parameters);
  18. }

如果需要对 session 进行加密,那么就会创建一个 EncryptedStore 类,该类继承 Store 类。

setId

session 驱动建立之后,就要进行 sessionId 的设置,如果 cookie 中存在 sessionId,我们就会从中获取,否则我们就需要重新生成新的 sessionId

  1. public function setId($id)
  2. {
  3. $this->id = $this->isValidId($id) ? $id : $this->generateSessionId();
  4. }
  5. public function isValidId($id)
  6. {
  7. return is_string($id) && ctype_alnum($id) && strlen($id) === 40;
  8. }
  9. protected function generateSessionId()
  10. {
  11. return Str::random(40);
  12. }

session—start

一切准备就绪后,我们就要启动 session,如果当前请求存在未过期 session,那么就要利用 session 驱动将数据读取出来:

  1. public function start()
  2. {
  3. $this->loadSession();
  4. if (! $this->has('_token')) {
  5. $this->regenerateToken();
  6. }
  7. return $this->started = true;
  8. }
  9. protected function loadSession()
  10. {
  11. $this->attributes = array_merge($this->attributes, $this->readFromHandler());
  12. }

readFromHandler 函数就是读取 session 的过程:

  1. protected function readFromHandler()
  2. {
  3. if ($data = $this->handler->read($this->getId())) {
  4. $data = @unserialize($this->prepareForUnserialize($data));
  5. if ($data !== false && ! is_null($data) && is_array($data)) {
  6. return $data;
  7. }
  8. }
  9. return [];
  10. }
  • 未加密 session 数据的加载

对于未加密的 session 来说,prepareForUnserialize 直接返回了数据:

  1. protected function prepareForUnserialize($data)
  2. {
  3. return $data;
  4. }
  • 加密 session 数据
  1. protected function prepareForUnserialize($data)
  2. {
  3. try {
  4. return $this->encrypter->decrypt($data);
  5. } catch (DecryptException $e) {
  6. return serialize([]);
  7. }
  8. }
  • file 驱动
  1. public function read($sessionId)
  2. {
  3. if ($this->files->exists($path = $this->path.'/'.$sessionId)) {
  4. if (filemtime($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) {
  5. return $this->files->get($path, true);
  6. }
  7. }
  8. return '';
  9. }
  • redis 驱动
  1. public function read($sessionId)
  2. {
  3. return $this->cache->get($sessionId, '');
  4. }

session 垃圾回收

session 的垃圾回收用于随机性地删除旧 session 数据。由于某些驱动,例如 FileSessionHandler, 程序不会定期删除那些已经过时的 session 文件,那么 session 文件一定会越来越多,所以我们就需要一种垃圾回收机制:

  1. protected function collectGarbage(Session $session)
  2. {
  3. $config = $this->manager->getSessionConfig();
  4. if ($this->configHitsLottery($config)) {
  5. $session->getHandler()->gc($this->getSessionLifetimeInSeconds());
  6. }
  7. }
  8. protected function configHitsLottery(array $config)
  9. {
  10. return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
  11. }

configHitsLottery 函数就是判断当前是否被随机要进行垃圾回收任务。这种随机性概率由 lottery 来设置。

FileSessionHandler 的垃圾回收:

  1. public function gc($lifetime)
  2. {
  3. $files = Finder::create()
  4. ->in($this->path)
  5. ->files()
  6. ->ignoreDotFiles(true)
  7. ->date('<= now - '.$lifetime.' seconds');
  8. foreach ($files as $file) {
  9. $this->files->delete($file->getRealPath());
  10. }
  11. }

存储前一页

很多时候我们都需要从 session 中获取前一页的地址,例如用户授权失败就会返回上一页等等情景。

  1. protected function storeCurrentUrl(Request $request, $session)
  2. {
  3. if ($request->method() === 'GET' && $request->route() && ! $request->ajax()) {
  4. $session->setPreviousUrl($request->fullUrl());
  5. }
  6. }
  7. public function setPreviousUrl($url)
  8. {
  9. $this->put('_previous.url', $url);
  10. }

中间件的结束

当请求结束时,会调用中间件的 terminate 函数,这里程序会将新的 session 数据持久化到各个驱动器中:

  1. public function terminate($request, $response)
  2. {
  3. if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
  4. $this->manager->driver()->save();
  5. }
  6. }

session 的保存:

  1. public function save()
  2. {
  3. $this->ageFlashData();
  4. $this->handler->write($this->getId(), $this->prepareForStorage(
  5. serialize($this->attributes)
  6. ));
  7. $this->started = false;
  8. }

session 的保存会删除需要 flash 的闪存数据,也就是只想用于下一次请求的数据:

  1. public function ageFlashData()
  2. {
  3. $this->forget($this->get('_flash.old', []));
  4. $this->put('_flash.old', $this->get('_flash.new', []));
  5. $this->put('_flash.new', []);
  6. }

对于不加密的数据,保存前的 prepareForStorage 不会对数据进行任何操作:

  1. protected function prepareForStorage($data)
  2. {
  3. return $data;
  4. }

对于加密的数据,则需要事先加密:

  1. protected function prepareForStorage($data)
  2. {
  3. return $this->encrypter->encrypt($data);
  4. }

session 数据操作

get 函数

当我们想要获取 session 中的数据时,我们经常使用 get 方法

  1. public function show(Request $request, $id)
  2. {
  3. $value = $request->session()->get('key');
  4. //
  5. }

get 方法首先会调用 sessionManager 的魔术方法:

  1. public function __call($method, $parameters)
  2. {
  3. return $this->driver()->$method(...$parameters);
  4. }

driver 函数会返回 Store 对象,调用 get 方法

  1. public function get($key, $default = null)
  2. {
  3. return Arr::get($this->attributes, $key, $default);
  4. }

我们从上一节知道,在 startSession 中间件启动后,session 数据已经加载到了 store 对象中,因此获取数据很简单:

  1. public function get($key, $default = null)
  2. {
  3. return Arr::get($this->attributes, $key, $default);
  4. }

all 函数

all 函数可以取出所有的 session 数据

  1. public function all()
  2. {
  3. return $this->attributes;
  4. }

has 函数

要确定 Session 中是否存在某个值,可以使用 has 方法。如果该值存在且不为 null,那么 has 方法会返回 true

  1. public function has($key)
  2. {
  3. return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) {
  4. return is_null($this->get($key));
  5. });
  6. }

exists 函数

要确定 Session 中是否存在某个值,即使其值为 null,也可以使用 exists 方法。如果值存在,则 exists 方法返回 true

  1. public function exists($key)
  2. {
  3. return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) {
  4. return ! Arr::exists($this->attributes, $key);
  5. });
  6. }

put 方法

要存储数据到 Session,可以使用 put 方法

  1. public function put($key, $value = null)
  2. {
  3. if (! is_array($key)) {
  4. $key = [$key => $value];
  5. }
  6. foreach ($key as $arrayKey => $arrayValue) {
  7. Arr::set($this->attributes, $arrayKey, $arrayValue);
  8. }
  9. }

push 方法

push 方法可以将一个新的值添加到 Session 数组内。

  1. public function push($key, $value)
  2. {
  3. $array = $this->get($key, []);
  4. $array[] = $value;
  5. $this->put($key, $array);
  6. }

remember 方法

remember 方法用于有即取,无即存的情况:

  1. public function remember($key, Closure $callback)
  2. {
  3. if (! is_null($value = $this->get($key))) {
  4. return $value;
  5. }
  6. return tap($callback(), function ($value) use ($key) {
  7. $this->put($key, $value);
  8. });
  9. }

increment 方法

increment 方法用于增加某 session 数据的值:

  1. public function increment($key, $amount = 1)
  2. {
  3. $this->put($key, $value = $this->get($key, 0) + $amount);
  4. return $value;
  5. }

decrement 方法

  1. public function decrement($key, $amount = 1)
  2. {
  3. return $this->increment($key, $amount * -1);
  4. }

pull 方法

pull 方法可以只用一条语句就从 Session 检索并且删除一个项目:

  1. public function pull($key, $default = null)
  2. {
  3. return Arr::pull($this->attributes, $key, $default);
  4. }

flash 闪存数据

有时候你仅想在下一个请求之前在 Session 中存入数据,你可以使用 flash 方法。使用这个方法保存在 session 中的数据,只会保留到下个 HTTP 请求到来之前,然后就会被删除。闪存数据主要用于短期的状态消息

  1. public function flash($key, $value)
  2. {
  3. $this->put($key, $value);
  4. $this->push('_flash.new', $key);
  5. $this->removeFromOldFlashData([$key]);
  6. }
  7. protected function removeFromOldFlashData(array $keys)
  8. {
  9. $this->put('_flash.old', array_diff($this->get('_flash.old', []), $keys));
  10. }

闪存数据的实现很简单,session 中会维护两个数组:_flash.new_flash.old ,每次 session 结束前,都会删除 _flash.old 中的存储的 key 对应存储在 sessionvalue

now 方法

now 方法用于存储只有本次请求采用的数据

  1. public function now($key, $value)
  2. {
  3. $this->put($key, $value);
  4. $this->push('_flash.old', $key);
  5. }

reflash 方法

如果需要保留闪存数据给更多请求,可以使用 reflash 方法,这将会将所有的闪存数据保留给其他请求。

  1. public function reflash()
  2. {
  3. $this->mergeNewFlashes($this->get('_flash.old', []));
  4. $this->put('_flash.old', []);
  5. }

这样,_flash.old 中的数据就会被合并到 _flash.new 中。

keep 方法

只想保留特定的闪存数据给更多请求,则可以使用 keep 方法:

  1. public function keep($keys = null)
  2. {
  3. $this->mergeNewFlashes($keys = is_array($keys) ? $keys : func_get_args());
  4. $this->removeFromOldFlashData($keys);
  5. }

forget 方法

forget 方法可以从 Session 内删除一条数据。

  1. public function forget($keys)
  2. {
  3. Arr::forget($this->attributes, $keys);
  4. }

flush 方法

如果你想删除 Session 内所有数据,可以使用 flush 方法:

  1. public function flush()
  2. {
  3. $this->attributes = [];
  4. }

重新生成 Session ID

重新生成 Session ID,通常是为了防止恶意用户利用 session fixation 对应用进行攻击。如果使用了内置函数 LoginController,Laravel 会自动重新生成身份验证中 Session ID。否则,你需要手动使用 regenerate 方法重新生成 Session ID

  1. public function regenerate($destroy = false)
  2. {
  3. return $this->migrate($destroy);
  4. }
  5. public function migrate($destroy = false)
  6. {
  7. if ($destroy) {
  8. $this->handler->destroy($this->getId());
  9. }
  10. $this->setExists(false);
  11. $this->setId($this->generateSessionId());
  12. return true;
  13. }